package server;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

import server.IDInstance.IDFile;

public class IDFtpThread implements Runnable
{
	private Socket sock = null;
	private IDInstance env = null;
	private IDFtpListener server = null;
	
	private Thread myThread = null;
	
	public IDFtpThread(Socket sock, IDInstance env, IDFtpListener server)
	{
		this.sock = sock;
		this.env = env;
		this.server = server;
		
		myThread = new Thread(this);
	}
	
	public void startThread()
	{
		myThread.start();
	}
	
	public static boolean serveStream(String name, InputStream is, OutputStream os) throws UnsupportedEncodingException, IOException
	{
		int bufferMax = (int)Math.min(is.available(), 256 * 1024);
		byte[] buffer = new byte[bufferMax];
		
		while(true)
		{
			int rb = is.read(buffer);
			if(rb > 0)
			{
				os.write(buffer, 0, rb);
			}
			else
			{
				break;
			}
		}
		
		is.close();
		os.close();
		return true;
	}
	
	public static boolean serveFile(File file, OutputStream os)
	{
		if(file == null || !file.exists())
		{
			try {
				os.close();
			} catch (IOException e) {}
			return false;
		}
		
		System.out.println("Serving file: '" + file.getName() + "'");
		
		try 
		{
			FileInputStream fis = new FileInputStream(file);
			
			try
			{
				serveStream(file.getName(), fis, os);
			}
			catch(Exception e)
			{
				System.out.println("Fail at serving " + file.getName());
			}
		} 
		catch (IOException e) 
		{
			System.out.println("FAIL : " + e.getMessage());
			return false;
		}
		
		System.out.println("File served: '" + file.getName() + "'");
		
		return true;
	}
	
	public static String collapseSlashes(String input)
	{
		StringBuilder sb = new StringBuilder();
		char[] array = input.toCharArray();
		
		boolean lastSlash = false;
		for(char c : array)
		{
			if(c == '/' || c == '\\')
			{
				if(!lastSlash)
				{
					lastSlash = true;
					sb.append(c);
				}
			}
			else
			{
				lastSlash = false;
				sb.append(c);
			}
		}
		
		return sb.toString();
	}
	
	private ServerSocket passiveSock = null;
	private Thread passvThread = null;
	private volatile boolean passvRunning = false;
	private volatile boolean passvAccepted = false;
	private Socket passvSock = null;
	
	private int openPassiveSocket()
	{
		if(passiveSock != null)
		{
			try 
			{
				passiveSock.close();
			} 
			catch (IOException e) 
			{
				System.out.println("Error in closing previous socket.");
				return -1;
			}
			passiveSock = null;
		}
		
		int tries = 100;
		int port = 2001;
		boolean autoIncrement = true;
		
		while(true)
		{
			try 
			{
				passiveSock = new ServerSocket(port);
				break;
			} 
			catch (IOException e) 
			{
				if(autoIncrement)
				{
					tries--;
					if(tries > 0)
					{
						port++;
						continue;
					}
					else
					{
						System.out.println("Can't bind port");
						return -1;
					}
				}
				else
				{
					System.out.println("Can't bind port");
					return -1;
				}
			}
		}
		
		passvThread = new Thread(new Runnable()
		{
			
			@Override
			public void run()
			{
				try
				{
					passvSock = passiveSock.accept();
					passvAccepted = true;
					System.out.println("PASSV accepted!");
					System.out.flush();
				} 
				catch (IOException e)
				{
					//System.out.println("FAIL FAIL");
				}
			}
		});
		
		passvThread.start();
		
		
		return port;
	}
	
	@SuppressWarnings("deprecation")
	private static String constructDate(Date date)
	{
		String month = "";
		switch(date.getMonth())
		{
		case 0: month = "Jan"; break;
		case 1: month = "Feb"; break;
		case 2: month = "Mar"; break;
		case 3: month = "Apr"; break;
		case 4: month = "May"; break;
		case 5: month = "Jun"; break;
		case 6: month = "Jul"; break;
		case 7: month = "Aug"; break;
		case 8: month = "Sep"; break;
		case 9: month = "Oct"; break;
		case 10: month = "Nov"; break;
		case 11: month = "Dec"; break;
		}
		
		return month + " " + date.getDate() + " " + date.getHours() + ":" + date.getMinutes();
	}
	
	private static void printUnixListing(PrintWriter pw, IDFile dir, boolean aSwitch)
	{
		if(!dir.dir) return;
		
		Date date = new Date();
		
		if(aSwitch)
		{
			pw.println("drwxr-xr-x " + (dir.children.size() + 2) + " user group 4096 " + constructDate(dir.date) + " .");
			if(dir.parent != null)
			{
				pw.println("drwxr-xr-x " + (dir.parent.children.size() + 2) + " user group 4096 " + constructDate(dir.parent.date) + " ..");
			}
		}
		
		for(IDFile file : dir.children)
		{
			StringBuilder sb = new StringBuilder();
			if(file.dir)
				sb.append('d');
			else
				sb.append('-');
			
			sb.append("rwxr-xr-x ");
			
			if(file.dir)
				sb.append(file.children.size() + 2);
			else
				sb.append(1);
			
			sb.append(' ');
			
			sb.append("user group ");
			sb.append(file.size);
			sb.append(' ');
		
			if(file.date != null)
				sb.append(constructDate(file.date));
			else
				sb.append(constructDate(date));
			
			sb.append(' ');
			sb.append(file.name);
			
			pw.println(sb.toString());
		}
		
		pw.flush();
	}

	@SuppressWarnings("deprecation")
	@Override
	public void run() 
	{
		System.out.println("Started an FTP thread...");
		
		BufferedReader in;
		try 
		{
			in = new BufferedReader(new InputStreamReader(
			    sock.getInputStream()));
		
	        PrintWriter out = new PrintWriter(new OutputStreamWriter(sock.getOutputStream(), "UTF-8")); //new PrintWriter(sock.getOutputStream());
	
	        String str = ".";
	        
	        // welcome message
	        out.println("220 Welcome to InstaDir FTP server!");
	        out.flush();
	        System.out.println("Sent welcome message");
	        
	        boolean utf8enabled = false;
	        String dataType = "";
	        boolean binaryMode = false;
	        boolean passiveMode = false;
	        int passivePort = -1;
	        
	        String cwd = "/";
	        IDFile eCwd = env.getRoot();
	        
	        while (!str.equals(""))
	        {
	          str = in.readLine();
	          if(str == null)
	        	  break;
	          
	          System.out.println("FTP: \"" + str + "\"");
	          System.out.flush();
	          
	          String upperStr = str.toUpperCase();
	          
	          if(upperStr.startsWith("AUTH "))
	          {
	        	  System.out.println("Auth requested, denying");
	        	  out.println("500 No authentification mechanisms are implemented here");
	        	  out.flush();
	          }
	          else if(upperStr.startsWith("USER "))
	          {
	        	  System.out.println("User login supplied, unnecessary");
	        	  out.println("230 If you say so...");
	        	  out.flush();
	          }
	          else if(upperStr.startsWith("PASS "))
	          {
	        	  System.out.println("User password supplied, unnecessary");
	        	  out.println("230 Sure thing...");
	        	  out.flush();
	          }
	          else if(upperStr.startsWith("SYST"))
	          {
	        	  System.out.println("System type requested");
	        	  out.println("215 UNIX Type: L8");
	        	  out.flush();
	          }
	          else if(upperStr.startsWith("FEAT"))
	          {
	        	  System.out.println("Features requested, supporting UTF-8");
	        	  //out.println("500 I has no features :(");
	        	  out.println("211- Features");
	        	  out.println(" UTF8");
	        	  out.println("211 end");
	        	  out.flush();
	          }
	          else if(upperStr.startsWith("OPTS "))
	          {
	        	  String tokens[] = str.substring(5, str.length()).split(" ");
	        	  if(tokens.length == 0)
	        	  {
	        		  System.out.println("LOLWUT? Empty OPTS");
	        		  out.println("501 Wtf is this?");
	        		  out.flush();
	        	  }
	        	  else
	        	  {
	        		  if(tokens[0].toUpperCase().equals("UTF8"))
	        		  {
	        			  if(tokens.length == 2)
	        			  {
	        				  if(tokens[1].toUpperCase().equals("ON"))
	        				  {
	        					  utf8enabled = true;
	        					  System.out.println("UTF-8 enabled by the client");
	        					  out.println("230 Allrighty then!");
	        					  out.flush();
	        				  }
	        				  else if(tokens[1].toUpperCase().equals("OFF"))
	        				  {
	        					  utf8enabled = false;
	        					  System.out.println("UTF-8 disabled? :(");
	        					  out.println("230 Y U NO UTF-8? :'(");
	        					  out.flush();
	        				  }
	        				  else
	        				  {
	        					  System.out.println("UTF8 isn't either ON or OFF? O_o");
	        	        		  out.println("500 Wtf is this?");
	        	        		  out.flush();
	        				  }
	        			  }
	        			  else
	        			  {
	        				  System.out.println("UTF8 isn't either ON or OFF? O_o");
        	        		  out.println("500 Wtf is this?");
        	        		  out.flush();
	        			  }
	        		  }
	        		  else
	        		  {
	        			  System.out.println("Unrecognized option...");
    	        		  out.println("502 No comprende...");
    	        		  out.flush();
	        		  }
	        	  }
	          }
	          else if(upperStr.equals("PWD"))
	          {
	        	  System.out.println("Print working directory... [" + cwd + "]");
	        	  if(cwd.startsWith("/"))
	        		  out.println("200 " + cwd);
	        	  else
	        		  out.println("200 /" + cwd);
	        	  out.flush();
	          }
	          else if(upperStr.startsWith("TYPE "))
    		  {
	        	  dataType = str.substring(5, str.length());
	        	  if(dataType.equals("I"))
	        	  {
	        		  binaryMode = true;
	        		  System.out.println("Data type set to BINARY");
	        		  out.println("200 OK");
		        	  out.flush();
	        	  }
	        	  else if(dataType.equals("A"))
	        	  {
	        		  binaryMode = true;
	        		  System.out.println("Data type set to ASCII");
	        		  out.println("200 OK");
		        	  out.flush();
	        	  }
	        	  else
	        	  {
	        		  out.println("501 LOLWUT?");
		        	  out.flush();
	        	  }
    		  }
	          else if(upperStr.equals("EPSV"))
	          {
	        	  System.out.println("EPSV denied");
	        	  out.println("425 Can't do EPSV");
	        	  out.flush();
	          }
	          else if(upperStr.equals("PASV"))
	          {
	        	  System.out.println("Entering passive mode...");
	        	  String localIP = sock.getLocalAddress().getHostAddress();
	        	  if(localIP.startsWith("/")) localIP = localIP.substring(1, localIP.length());
	        	  String parts[] = localIP.split("\\.");
	        	  
	        	  passivePort = openPassiveSocket();
	        	 
	        	  if(passivePort > 0)
	        	  {
	        		  passiveMode = true;
	        		  System.out.println("227 Entering Passive Mode (" + parts[0] + "," + parts[1] + "," + parts[2] + 
		        			  "," + parts[3] + "," + ((passivePort >> 8) & 0xFF) + "," + (passivePort & 0xFF) +  ")");
	        		  
	        		  out.println("227 Entering Passive Mode (" + parts[0] + "," + parts[1] + "," + parts[2] + 
		        			  "," + parts[3] + "," + ((passivePort >> 8) & 0xFF) + "," + (passivePort & 0xFF) +  ")");
		        	  out.flush();
	        	  }
	        	  else
	        	  {
	        		  passiveMode = false;
	        		  System.out.println("Couldn't create passive socket");
	        		  out.println("421 Can't create socket");
	        		  out.flush();
	        	  }
	          }
	          else if(upperStr.startsWith("CWD "))
	          {
	        	  String targetDir = str.substring(4, str.length());
	        	  System.out.println("Requesting CWD to \"" + targetDir + "\"");
	        	  
	        	  IDFile newRoot = null;
	        	  
	        	  if(targetDir.startsWith("/"))
	        		  newRoot = env.traversePath(targetDir);
	        	  else
	        		  newRoot = env.traversePath(cwd + "/" + targetDir);
	        	  
	        	  if(newRoot == null)
	        	  {
	        		  System.out.println("Couldn't find requested DIR!");
	        		  out.println("431 Invalid path / non-existent directory");
	        		  out.flush();
	        	  }
	        	  else
	        	  {
	        		  if(newRoot.dir)
	        		  {
	        			  cwd = newRoot.relPath;
	        			  if(!cwd.startsWith("/")) cwd = "/" + cwd;
	        			  eCwd = newRoot;
	        			  System.out.println("CWD set to \"" + targetDir + "\"");
	        			  out.println("200 OK");
	    	        	  out.flush();
	        		  }
	        		  else
	        		  {
	        			  System.out.println("Path is a file");
	        			  out.println("431 Invalid path / non-existent directory");
	        			  out.flush();
	        		  }
	        	  }
	          }
	          else if(upperStr.equals("CDUP"))
        	  {
	        	  if(eCwd.parent != null)
	        	  {
	        		  eCwd = eCwd.parent;
	        		  cwd = eCwd.relPath;
	        		  if(cwd.length() == 0)
	        			  cwd = "/";
	        		  System.out.println("CWD set to \"" + cwd + "\"");
        			  out.println("200 OK");
    	        	  out.flush();
	        	  }
	        	  else
	        	  {
	        		  System.out.println("Can't go further up!");
	        		  out.println("431 Invalid path / non-existent directory");
	        		  out.flush();
	        	  }
        	  }
	          else if(upperStr.startsWith("LIST"))
	          {
	        	  String listPath = ".";
	        	  boolean aswitch = false;
	          
	        	  if(upperStr.length() > 5)
	        	  {
		        	  String paramPart = str.substring(5, str.length());
		        	  
		        	  while(paramPart.startsWith("-"))
		        	  {
		        		  int endChar = paramPart.length();
		        		  for(int i = 1; i < paramPart.length(); ++i)
		        		  {
		        			  if(paramPart.charAt(i) == ' ')
		        			  {
		        				  endChar = i;
		        				  break;
		        			  }
		        		  }
		        		  
		        		  String param = paramPart.substring(1, endChar);
		        		  if(endChar < paramPart.length() - 1)
		        			  paramPart = paramPart.substring(endChar + 1, paramPart.length());
		        		  else
		        			  paramPart = "";
		        		  
		        		  if(param.equals("a"))
		        		  {
		        			  System.out.println("List param - all");
		        			  aswitch = true;
		        		  }
		        	  }
		        	  
		        	  if(paramPart.length() > 0)
		        		  paramPart = paramPart.trim();
		        	  
		        	  if(paramPart.length() > 0)
		        	  {
		        		  listPath = paramPart;
		        		  System.out.println("LIST PATH = \"" + paramPart + "\"");
		        	  }
	        	  }
	        	  
	        	  if(listPath.equals("."))
	        	  {
	        		  listPath = cwd;
	        		  if(!listPath.startsWith("/"))
	        			  listPath = "/" + listPath;
	        	  }
	        	  
	        	  IDFile listTarget = null; //env.traversePath(listPath);
	        	  
	        	  System.out.println("List CWD        = \"" + cwd + "\"");
	        	  System.out.println("List final path = \"" + listPath + "\"");
	        	  
	        	  if(listPath.startsWith("/"))
	        		  listTarget = env.traversePath(listPath);
	        	  else
	        		  listTarget = env.traversePath(cwd + "/" + listPath);
	        	  
	        	  if(listTarget != null)
	        	  {
	        		  if(passvSock == null)
	        		  {
	        			  openPassiveSocket();
	        		  }
	        		  
	        		  if(!passvAccepted)
	        		  {
		        		  for(int i = 0; i < 10; ++i)
		        		  {
		        			  if(passvAccepted)
		        				  break;
		        			  else
		        			  {
		        				try
								{
									Thread.sleep(100);
								} 
		        				catch (InterruptedException e)
								{}
		        			  }
		        		  }
	        		  }
	        		  
	        		  System.out.println("Should output list for \"" + listPath + "\"...");
	        		  if(passvSock != null)
	        		  {
	        			  out.println("150 Opening BINARY mode data connection for file list");
	        			  out.flush();
	        			  printUnixListing(new PrintWriter(passvSock.getOutputStream()), listTarget, aswitch);
	        			  passvSock.close();
	        			  passvSock = null;
	        			  passvAccepted = false;
	        			  
	        			  out.println("226 Transfer complete");
		        		  out.flush();
		        		  System.out.println("List dumped!");
		        		  System.out.flush();
	        		  }
	        		  else
	        		  {
	        			  
	        			  out.println("500 Major fail");
	        			  out.flush();
	        			  System.out.println("Passv socket isn't working");
	        			  /*
	        			  printUnixListing(out, listTarget, aswitch);
	        			  out.println("250 OK");
		        		  out.flush();
		        		  System.out.println("List dumped!");
		        		  System.out.flush();
		        		  */
	        		  }
	        	  }
	        	  else
	        	  {
	        		  System.out.println("Invalid path!");
	        		  out.println("431 Invalid path / non-existent directory");
	        		  out.flush();
	        	  }
	          }
	          else if(upperStr.startsWith("RETR "))
	          {
	        	  String fileName = str.substring(5, str.length());
	        	  if(fileName.length() == 0)
	        	  {
	        		  out.println("501 No file name?");
		        	  out.flush();
	        	  }
	        	  else
	        	  {
	        		  IDFile targetFile = null;
	        		  
	        		  System.out.println("fileName = \"" + fileName + "\"");
	        		  
	        		  if(fileName.startsWith("/"))
	        			  targetFile = env.traversePath(fileName);
	        		  else
	        			  targetFile = env.traversePath(cwd + "/" + fileName);
	        			  
	        		  if(targetFile != null)
	        		  {
	        			  System.out.println("File found, should begin transfer...");
	        			  
	        			  if(passvSock == null)
		        		  {
		        			  openPassiveSocket();
		        		  }
		        		  
		        		  if(!passvAccepted)
		        		  {
			        		  for(int i = 0; i < 30; ++i)
			        		  {
			        			  if(passvAccepted)
			        				  break;
			        			  else
			        			  {
			        				try
									{
										Thread.sleep(100);
									} 
			        				catch (InterruptedException e)
									{}
			        			  }
			        		  }
		        		  }
		        		  
		        		  if(passvSock != null)
		        		  {
		        			  out.println("150 Opening BINARY mode data connection for file transfer");
		        			  out.flush();
		        			  //printUnixListing(new PrintWriter(passvSock.getOutputStream()), listTarget, aswitch);
		        			  
		        			  serveFile(targetFile.file, passvSock.getOutputStream());
		        			  
		        			  passvSock.close();
		        			  passvSock = null;
		        			  passvAccepted = false;
		        			  
		        			  out.println("226 Transfer complete");
			        		  out.flush();
			        		  System.out.println("File sent!");
			        		  System.out.flush();
		        		  }
		        		  else
		        		  {
		        			  
		        			  out.println("500 Major fail");
		        			  out.flush();
		        			  System.out.println("Passv socket isn't working");
		        			  /*
		        			  printUnixListing(out, listTarget, aswitch);
		        			  out.println("250 OK");
			        		  out.flush();
			        		  System.out.println("List dumped!");
			        		  System.out.flush();
			        		  */
		        		  }
	        		  }
	        		  else
	        		  {
	        			  System.out.println("File \"" + targetFile + "\" not found!");
	        			  out.println("550 Invalid path / non-existent file");
		        		  out.flush();
	        		  }
	        	  }
	          }
	          else
	          {
	        	  System.out.println("Unknown command, ignoring");
	        	  out.println("202 LOLWUT?");
	        	  out.flush();
	          }
	        }
	        
	        
	        out.flush();
	        sock.close();
		} 
		catch (IOException e) 
		{
			System.out.println("Some kind of fail : " + e.getMessage());
		}
		
		if(passvThread != null)
			passvThread.stop();
		
		System.out.println("Ended an FTP thread.");
		server.unregisterThread(this);
	}
}
